package com.xiao.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiao.domain.constants.ArticleConstants;
import com.xiao.domain.ResponseResult;
import com.xiao.domain.dto.AddArticleDto;
import com.xiao.domain.dto.article.QueryArticleDto;
import com.xiao.domain.dto.article.SaveUpdateArticleDto;
import com.xiao.domain.pojo.ArticleCategory;
import com.xiao.domain.pojo.BlogArticleTag;
import com.xiao.domain.vo.article.ArticleListVo;
import com.xiao.domain.vo.article.UpdateArticleVo;
import com.xiao.enums.AppHttpCodeEnum;
import com.xiao.mapper.ArticleMapper;
import com.xiao.domain.pojo.Article;
import com.xiao.mapper.BlogArticleTagMapper;
import com.xiao.service.ArticleCategoryService;
import com.xiao.service.ArticleService;
import com.xiao.service.BlogArticleTagService;
import com.xiao.utils.BeanCopyUtils;
import com.xiao.utils.RedisCache;
import com.xiao.domain.vo.article.ArticleDetailsVo;
import com.xiao.domain.vo.article.ArticleSnapshotListVo;
import com.xiao.domain.vo.HotArticleVo;
import com.xiao.domain.vo.PageVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article> implements ArticleService {

    @Autowired // 文章分类 service
    ArticleCategoryService articleCategoryService;

    @Autowired // redis 工具
    RedisCache redisCache;
    @Override// 查询热门文章
    public ResponseResult hosArticleList() {
        // 查询热门文章 封装成 ResponseResult 返回
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
        // 0 已发布，1 草稿 必须是已发布的正式文章
        wrapper.eq(Article::getStatus, ArticleConstants.STATUS_RELEASED); // mybatis_plus 条件构造器 相当于  blog_article.status=0 文章已发表的
        // 按照浏览量进行排序
        wrapper.orderByDesc(Article::getViewCount);// Java 8 中引入的 method reference 语法，等价于 Article.class.getMethod("getViewCount")

        // 最多只查询 10 条; 当前显示第一页的数据，每页显示10条数据
        Page<Article> page = new Page(ArticleConstants.HOT_CURRENT_INDEX, ArticleConstants.HOT_VIEW_COUNT);// 不要导错包

        page(page,wrapper);
        // 最终查询结果
        List<Article> articles = page.getRecords();

        // -------- Vo 优化 Start -----------
        List<HotArticleVo> hotArticleVoList = new ArrayList<>(); // 1. 创建接受接收数据容器
        for (Article article : articles) {  // 2. 遍历数据源
            HotArticleVo hotArticleVo = new HotArticleVo();// 3. 创建接收数据实例

            // ------------ 从 redis 中读取最新数据 Start ----------------
            // 这里尤其注意，我们是在项目启动时，向 redis 传递数据的，但是如果文章是 项目启动后 发表的呢? 这里就会是 viewCount  null  !!! 我们需要预防
            Integer viewCount = redisCache.getCacheMapValue(ArticleConstants.ARTICLE_VIEW_COUNT, article.getId().toString());
            // viewCount 是null ，因此 调用 viewCount.longValue() 一定是 nullPointException 空指针异常!!!

            if (Objects.isNull(viewCount)){ // 如果在 redis 中没有这个文章数据，那么先设为 0 。
                viewCount=0;
            }

            article.setViewCount(viewCount.longValue());
            // ------------ 从 redis 中读取最新数据 End ----------------

            //【 bean 拷贝】JavaBean 对象的属性值复制到另一个 JavaBean 对象中，前提是这两个对象中的属性具有相同的名称和兼容的类型
            BeanUtils.copyProperties(article,hotArticleVo); // 4. copy数据到 接收数据实例  不要导错包 org.springframework.beans.BeanUtils;

            hotArticleVoList.add(hotArticleVo); // 5. 装载 接收数据实例
        } // 后续会把 bean 拷贝封装为一个方法
        // -------- Vo 优化 End -----------
        return ResponseResult.okResult(hotArticleVoList);// 返回我们的 Vo 列表
    }



    @Override// 文章分页实现
    public ResponseResult articleList(Integer pageNum, Integer pageSize, Long categoryId) {

        // 1. 编写查询条件
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
        // 如果 categoryId!=null&&categoryId>0 为true, 才会限制 categoryId 否则不限制
        wrapper.eq(categoryId!=null&&categoryId>0,Article::getCategoryId,categoryId);// 条件 属性 值; 当条件满足时，才构建查询限制
        wrapper.eq(Article::getStatus,ArticleConstants.STATUS_RELEASED); // 文章状态已发布
        wrapper.orderByDesc(Article::getIsTop);// 置顶是1 非置顶 0 因此 倒序 置顶排前面

        // 2.分页查询
        Page<Article> articlePage = new Page<>(pageNum,pageSize);
        this.page(articlePage, wrapper);

        // 当前查询出来的数据，是没有 categoryName 的需要我们自己再查询
        List<Article> records = articlePage.getRecords();

        // 3. 利用 stream 操作数据,查询 categoryName


        List<Article> list = records.stream()
                .map(article ->
                        {
                            String categoryName=null;

                            Long id = article.getCategoryId();

                            // 遍历出来一个个 把 categoryName 查询出来遍历
                            if(id!=null&&id>0){ // 避免 分类为null 导致 NullPointException
                                categoryName=articleCategoryService.getById(id).getName();
                            }

                            article.setCategoryName(categoryName);
                            // ------------ 从 redis 中读取最新数据 Start ----------------
                            Integer viewCount =  redisCache.getCacheMapValue(ArticleConstants.ARTICLE_VIEW_COUNT, article.getId().toString());

                            if(viewCount==null){ // 如果是项目启动后发表的文章，那么就先设为 0 避免空指针异常
                                viewCount=0;
                            }
                            article.setViewCount(
                                    viewCount.longValue()
                            );
                            // ------------ 从 redis 中读取最新数据 End ----------------
                            return article;// map 返回的内容，就是我们流的内容,因此不能随意返回;
                        }
                ).collect(Collectors.toList());

        // 4. 封装成 VO
        List<ArticleSnapshotListVo> articleListVos = BeanCopyUtils.copyBeanList(list, ArticleSnapshotListVo.class);



        // 再次封装,加了一个 总数
        PageVo pageVo = new PageVo(articleListVos, articlePage.getTotal());

        return ResponseResult.okResult(pageVo);
    }

    @Override
    public ResponseResult getArticleDetailsById(Long id) {
        // 前端是根据这个方法，来获取文章详细信息的，包括 浏览量，因此我们改变这里，让他去 redis 中读取，以便获取最新的值(数据先存入 redis 定时更新到 mysql 因此，mysql 中并不是最新的数据)
        // 1. 根据ID 查询到文章
        Article byId = this.getById(id);

        // ------------ 从 redis 中读取最新数据 Start ----------------
        Integer viewCount = redisCache.getCacheMapValue(ArticleConstants.ARTICLE_VIEW_COUNT, id.toString());
        byId.setViewCount(viewCount.longValue());
        // ------------ 从 redis 中读取最新数据 End ----------------
        // 2. 转VO
        ArticleDetailsVo articleDetailsVo = BeanCopyUtils.copyBean(byId, ArticleDetailsVo.class);

        // 3.通过调用  articleCategoryService 根据分类ID 去查询 分类名称

        String categoryName=null;

        Long categoryId = articleDetailsVo.getCategoryId();

        if (categoryId!=null&&categoryId>0){
            ArticleCategory articleCategory = articleCategoryService.getById(categoryId);
            if (articleCategory!=null){
                categoryName=articleCategory.getName();
            }
        }

        articleDetailsVo.setCategoryName(categoryName);

        // 4. 返回统一响应格式
        ResponseResult responseResult = ResponseResult.okResult(articleDetailsVo);
        return responseResult;
    }

    @Override
    public ResponseResult updateViewCount(Long id) {
        // 更新 redis 中对应的 id 浏览量
        redisCache.incrementCacheValue(ArticleConstants.ARTICLE_VIEW_COUNT,id.toString(),1);
        return ResponseResult.okResult();
    }

    @Autowired // 文章与标签 关联接口
    BlogArticleTagService blogArticleTagService;

    @Override
    @Transactional // 声明事务; [因为我们这里做了,两个插入语句,他们是 一个事务,因此需要事务的支持]
    public ResponseResult addArticle(AddArticleDto addArticleDto) {
        Article article = BeanCopyUtils.copyBean(addArticleDto, Article.class);
        boolean isSave = this.save(article);

        ResponseResult responseResult;

        if (isSave) {
            responseResult = ResponseResult.okResult();
        } else {
            responseResult = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
        }

        // 当前文章与标签关联表;
        List<Long> tags = addArticleDto.getTags();
        if (tags.size()>0){
            // article.getId() 因为 mybatis_plus 会回填ID，因此我们能拿到他填充的ID。
            List<BlogArticleTag> blogArticleTag = tags.stream()
                    .map(tag -> new BlogArticleTag(article.getId(), tag.longValue()))
                    .collect(Collectors.toList());
            // 把当前文章ID和标签 关系插入数据库中
            blogArticleTagService.saveBatch(blogArticleTag);
        }


        return responseResult;
    }

    @Override
    public ResponseResult<PageVo> getArticleList(Integer pageNum, Integer pageSize, QueryArticleDto dto) {
        // 1. 构建查询条件
        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(StringUtils.hasText(dto.getSummary()),Article::getSummary,dto.getSummary());
        wrapper.like(StringUtils.hasText(dto.getTitle()),Article::getTitle,dto.getTitle());
        wrapper.orderByDesc(Article::getCreateTime);
        // 2. 构建分页
        Page<Article> page = new Page<>();
        page.setCurrent(pageNum);
        page.setSize(pageSize);
        Page<Article> articleList = this.page(page, wrapper);
        List<Article> records = articleList.getRecords();
        // 3. 转 vo
        List<ArticleListVo> listVos = BeanCopyUtils.copyBeanList(records, ArticleListVo.class);

        // 4. 封装 vo
        PageVo pageVo = new PageVo(listVos, page.getTotal());
        // 5. 统一响应
        ResponseResult<PageVo> responseResult =ResponseResult.okResult(pageVo);

        return responseResult;
    }

    @Autowired
    BlogArticleTagMapper blogArticleTagMapper;

    @Override
    public ResponseResult<UpdateArticleVo> updateArticle(Long id) {

        // 1. 根据文章ID 查询文章
        Article article = this.getBaseMapper().selectById(id);
        // 2. 根据文章ID,查询 标签列表
        List<String> tagList = blogArticleTagMapper.getTagListByArticleId(id);
        // 3. 封装 vo
        UpdateArticleVo updateArticleVo = BeanCopyUtils.copyBean(article, UpdateArticleVo.class);
        updateArticleVo.setTags(tagList);
        // 4. 统一响应
        ResponseResult responseResult = ResponseResult.okResult(updateArticleVo);

        return responseResult;
    }

    @Override
    @Transactional // 涉及到两个表了,需要添加事务支持
    public ResponseResult saveUpdateArticle(SaveUpdateArticleDto dto) {
        // 1. 跟新文章表数据
        Article article = BeanCopyUtils.copyBean(dto, Article.class);
        int i = this.getBaseMapper().updateById(article);
        // 2. 跟新文章与标签关联表数据
        // 2.1 删除该文章所有标签数据
        LambdaQueryWrapper<BlogArticleTag> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(BlogArticleTag::getArticleId,dto.getId());
        blogArticleTagMapper.delete(wrapper);// 因为如果他没有标签所以可以是0 所以我们不做判断

        // 2.2 插入当前关联的标签数据
        List<String> tags = dto.getTags();
        boolean isSave=true;
        if (tags.size()>0){
            List<BlogArticleTag> collect = tags.stream()
                    .map(tag -> new BlogArticleTag(dto.getId(), Long.valueOf(tag)))
                    .collect(Collectors.toList());
            isSave = blogArticleTagService.saveBatch(collect);
        }

        // 3. 统一响应
        ResponseResult responseResult;
        if (i>0&&isSave){
            responseResult=ResponseResult.okResult();
        }else {
            responseResult=ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
        }

        return responseResult;
    }

    @Override
    public ResponseResult delArticleByIdList(List<Long> idList) {

        int i = this.getBaseMapper().deleteBatchIds(idList);

        ResponseResult responseResult ;
        if (i>0){
            responseResult = ResponseResult.okResult();
        }else {
            responseResult = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
        }

        return responseResult;
    }

}
